package edu.cmu.cs211.bacon.kevin;

import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.LinkedList;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

/**
 * 
 * Parses well-formed, header-less IMDB actor and actress files.
 *
 */

public class ImdbParser {
    private static Pattern actorPattern = Pattern.compile("([^\t]+)\t+([^\t].*)");
    private static Pattern moviePattern = Pattern.compile("\t+([^\t].*)");
    private static final int ACTOR_FROM_ACTOR_GROUP = 1;
    private static final int MOVIE_FROM_ACTOR_GROUP = 2;
    private static final int MOVIE_FROM_MOVIE_GROUP = 1;

    private static Pattern emptyLine = Pattern.compile("\\s*");
    
    public static void main(String[] args) {
        Map<String,List<Movie>> map = new HashMap<String, List<Movie>>();

        PrintStream out;
        try {
            out = new PrintStream("foo.list");
        } catch (IOException e) {
            System.err.println("Could not open foo.list for writing.");
            return;
        }
        
        // The following two lines, if uncommented, would add all actors and actresses 
        // starting with "Baco" and all 1999 movie actors and actresses
        // starting with "Bac" to the map.
        
        // Please go to the assignment documentation on Frontdesk to
        // download these files. Then import them into your Eclipse project's
        // root directory.
        //parseImdbFile("1999bac.list", map, null);
        //parseImdbFile("baco.list", map, null);
        
        // The following line, if uncommented, would add all actors and actresses
        // from 1999 starting with "Ba" to the list.  
        //parseImdbFile("1999ba.list", null, out);
       
    }

    /**
     * 
     * Parses an IMDB-formated actor or actress file, minus the IMDB header
     * information.  This function is not robust:  slight changes in the
     * file format will result in parse errors.  The IMDB file must be formatted as
     * a tab-delimited actor/movie pair on a single line, followed by other movies
     * (preceeded by tabs) the actor appeared in (one per line), 
     * and finally followed by a blank
     * line.  After the blank line, another actor/movie block may begin, or the
     * file may end.  The file must end in a single blank line after an actor/movie
     * block.
     * 
     * @param fileName
     *      The name of the file to open and parse.
     *      
     * @param map
     *      A map to store the actor/movie-list pairs.
     *      If map is not null, parseImdbFile will add actor/movie-list pairs to the map.
     *      If map is null, out must not be null.
     *      
     * @param out
     *      A PrintStream to output the actor/movie-list pairs to.
     *      If out is null, map must not be null.
     *      
     * @throws NullPointerException
     *      If fileName is null, or if both map and out are null.
     */
    public static void parseImdbFile(String fileName, Map<String,List<Movie>> map, PrintStream out) {
        if (fileName == null)
            throw new NullPointerException("fileName was null");
        if (map == null && out == null)
            throw new NullPointerException("both map and printstream were null");
        
        BufferedReader br;
        try {
            br = new BufferedReader(new FileReader(fileName));
        } catch (FileNotFoundException e) {
            System.err.println("Could not open file " + fileName);
            return;
        }
        assert(br != null);
        
        String nextLine = getLine(br);
        assert (nextLine != null);
        while (Character.isISOControl(nextLine.charAt(0))) {
            nextLine = nextLine.substring(1);
        }

        while (nextLine != null) {
            List<Movie> movies = new LinkedList<Movie>();
            String actor = actorFromActorLine(nextLine);
            Movie movie = movieFromActorLine(nextLine);
            assert (actor != null && movie != null);
            movies.add(movie);

            nextLine = getLine(br);
            while (!emptyLine.matcher(nextLine).matches()) {
                movie = movieFromMovieLine(nextLine);
                assert (movie != null);
                movies.add(movie);
                nextLine = getLine(br);
            }
            
            if (!movies.isEmpty()) {
                if (map != null)
                    map.put(actor, movies);
                if (out != null)
                    printActor(actor, movies, out);
            }
            nextLine = getLine(br);
        }
    }
    
    private static String getLine(BufferedReader br) {
        try {
            return br.readLine();
        } catch (IOException e) {
            return null;
        }
    }
    
    private static String actorFromActorLine(String line) {
        Matcher actorMatcher = actorPattern.matcher(line);
        boolean matched = actorMatcher.matches();
        assert(matched);
        String nextActor = actorMatcher.group(ACTOR_FROM_ACTOR_GROUP);
        assert (nextActor != null);
        return nextActor;
    }

    private static Movie movieFromActorLine(String line) {
        Matcher actorMatcher = actorPattern.matcher(line);
        boolean matched = actorMatcher.matches();
        assert(matched);
        String movieString = actorMatcher.group(MOVIE_FROM_ACTOR_GROUP);
        return Movie.parseMovie(movieString);
    }
    
    private static Movie movieFromMovieLine(String line) {
        Matcher movieMatcher = moviePattern.matcher(line);
        boolean matched = movieMatcher.matches();
        assert(matched);
        String movieString = movieMatcher.group(MOVIE_FROM_MOVIE_GROUP);
        return Movie.parseMovie(movieString);
    }
    
    /**
     * Prints a IMDB movie map in a parseable format.
     * 
     * @param map
     *      The map to be printed.
     *      
     * @param out
     *      A PrintStream to print the map to.
     *      
     * @throws NullPointerException
     *      If map or out is null.
     */
    public static void printMap(Map<String,List<Movie>> map, PrintStream out) {
        if (out == null)
            throw new NullPointerException("out was null");
        if (map == null)
            throw new NullPointerException("map was null");
        
        for (String actor : map.keySet()) {
            printActor(actor, map.get(actor), out);
        }
    }
    
    /** 
     * Prints a single actor or actress block in a parseable format
     * 
     * @param actor
     *      The name of the actor or actress.
     *      
     * @param movies
     *      The movies the actor or actress appeared in.
     *      
     * @param out
     *      The PrintStream to print the actor or actress block to.
     *      
     * @throws NullPointerException
     *      If actor, movies, or out is null, or if the list contains a null movie.
     */
    public static void printActor(String actor, List<Movie> movies, PrintStream out) {
        if (out == null)
            throw new NullPointerException("out was null");
        if (actor == null)
            throw new NullPointerException("actor was null");
        if (movies == null) 
            throw new NullPointerException("movies was null");
       
        out.print(actor);
        for (Movie movie : movies)
            out.println("\t\t\t" + movie);
        out.println();
    }
}
